NPC Travel

(Adapted for Adv3Lite by Eric Eve)

Editor's Note: The adv3Lite way of handling this is sufficiently different from adv3 that little remains of MJR's original article here beyond the Introduction. The remainder is almost a complete re-write that ends up rather shorter.

Introduction

Many IF games contain people and creatures for the player to encounter. These are known as non-player characters, or NPCs. In real life, living creatures don't tend to stay forever rooted in a single location while the world carries on around them, so game authors frequently want to make their NPCs move around within the game world.

If you've ever written any IF before, you know that NPCs are inherently complicated to program, because you're trying to simulate creatures that are incredibly complex in real life. Fortunately, the basics of moving your characters around within a TADS 3 game world aren't too difficult to master. There are a few library methods that do most of the work for you; the only trick is to know which ones to call in which situations. This article covers the main methods of moving NPCs around and describes when to use each one.

moveInto

The simplest and most direct way to move actors around is with the moveInto() method.

moveInto() is a method of the actor you want to move, and you call it with the new location as the parameter:

   bob.moveInto(iceCave);

You should use moveInto() when you want to move an actor "by fiat" - that is, you want complete programmatic control over what happens, and what side effects occur. This routine simply makes the actor disappear from the old location and magically reappear in the new location. The library doesn't attempt to simulate the travel when you use this routine: there's no attempt to open any doors along the way, for example, and the library doesn't generate any messages mentioning that the actor is departing or arriving.

actionMoveInto

The difference between actionMoveInto() and moveInto() is that actionMoveInto also calls the notifyRemove and notifyInsert methods on the locations being moved from and to, marks the object (here the npc) as being moved, and marks it as having been seen by the player character if the player character can see it in its new location. You may or may not want these additional side-effects. If you do, use actionMoveInto(), otherwise use moveInto().

travelVia

The travelVia() method performs more "simulated" travel than moveInto or actionMoveInto do. Unlike moveInto(), the travelVia() method does carry out all of the standard notifications involved in the travel: it calls beforeTravel() on everything nearby in the starting location; it calls it calls travelerLeaving() on the starting location, which displays a departure message if the player character can see the actor departing; it calls noteTraversal() on the TravelConnector; it calls travelerEntering() on the new location; and it calls afterTravel() on everything nearby in the new location.

travelVia(conn. announceArrival?) is a method of the actor who's traveling, where conn is the connector being traversed, and announceArrival? (which defaults to true) is a flag that determines whether or not to display a message about the actor's arrival in the new location if the actor can be seen there by the player character.

Here's an example:

   local conn = bob.location.east;
   bob.travelVia(conn);

Note that travelVia() checks any travel barries that are in force on the connector, and do typically does carry out any pre-conditions of the travel. For example, if a closed door is in the way, travelVia() will make the character attempt to open it, and travel will be interrupted if it's locked. This is a plus and a minus, depending on what you're trying to accomplish; it's less useful when you want to move your characters by fiat, without regard to the simulation implications, help avoid creating jarring, unrealistic effects for the player.

Travel with Pathfinding - scriptedTravelTo()

The most full-featured travel method is the one that can use adv3Lite library's pathfinding module. This enables you to calculate the shortest route from one location to another, such as npc's current location to where you'd like them to move to. You could use this to script a quasi-automous npc who moves around the map in a realistic manner.

To initiate scripted travel of this sort, we just call scriptedTravelTo(dest) on the actor, e.g., bob.scriptedTravelTo(lighthouse). Alternatively we can define the path we want the actor to follow for ourselves. The dest parameter can be one of:

Once scripted travel has been initiated by scriptedTravelTo the actor's takeTurn() method will call tryScriptedTravel() on the actor once each turn to make the actor take the next step along the route, unless any of the following take precedence:

  1. The actor has conversed (with the player character) on the same turn (either by a NodeContinuation or because the player had directed a conversational command to the actor on that turn.
  2. The actor had executed an AgendaItem on the same turn.
  3. The actor's canEndConversation(endConvActor) method returns nil, which may, for example, be the case if sayBlockBye is defined on an active ConvNode or NodeEndCheck to prevent the actor wandering off at such a point in the conversation.

This behaviour is probably what we want most of the time. It allows us to interrupt the actor's scripted travel either by the player character conversing with the actor, if they happen to meet on the way, or by an AgendaItem that allows us to have the actor do something else along the route, or maybe stop travelling or setting out in a different direction if and when particular conditions obtain.

There may be some occasions when this isn't what we want, however. The most likely is when we set up scripted travel by calling getActor.scriptedTravelTo() from the invokeItem() method of an AgendaItem as we often might. Since tryScriptedTravel() won't be called on the same turn as an AgendaItem has been executed for the same actor, the actor won't start moving until the following turn. If that's not what we want, we could get round it by calling tryScriptedTravel() manually, but we can instead call scriptedTravelTo() with its second, optional, parameter set to true, e.g.,:

bobTravelToLighthouseAgenda: AgendaItem
   invokeItem()
   {
       getActor.scriptedTravelTo(lighthouse, true);
       isDone = true;
   }
   ...
;

This will force bob to start moving on the same turn. The other places we may need to do this are in the whenStarting(), whenEnding(), or eachTurn() method of a Scene (since these are called on Scenes after the takeTurn() methods of actors).

An actor's scripted travel will stop altogether (rather than just pause and resume) under any of the following three conditions:

As noted above, the dest parameter of scriptedTravelTo(dest) can be a Thing as well as a Room, in which case the actor will set out for the Room that contains the dest object. It does not, however, check whether the dest object subsequently moves, as it might if it's another actor, the player character, or an object being carried by another actor or the player character. If we want to endow our actor with tracking skills to follow such a moving target, we'd need to find a way to reissue the scriptedTravelTo() call each time the target object moves. One way to do this might be to use a Scene:

pursuitScene:Scene
   startsWhen = (whatever)
   
   whenStarting()
   {
      bob.scriptedTravelTo(gPlayerChar, true);
   }
   
   oldPcLoc = nil
   
   beforeAction
   {
      /*Note the player character's location*/
      oldPcLoc = gRoom;
   }
   
   afterAction()
   {
       if(gRoom != oldPcLoc)
          scriptedTravelTo(gRoom);
   }
   
   endsWhen = bob.isIn(gRoom)
   
   whenEnding()
   {
      bob.scriptedTravelTo(nil);
      ... 
      /* e.g. if we want bob to start following the player character from here. */
      bob.startFollowing();
   }

If we want Bob to keep following the player character once he's caught up with them, we need to use the method described in the next section; scriptedTravelTo() is intended for actor's moving around independently of the player character rather than for accompanying travel.

Accompanying and Following

Accompanying travel is where one actor accompanies another (usually the player character) on their travels, typically as a sidekick or guide. This requires rather different treatment from the methods discussed above.

If we want an NPC to follow the player character around as an accompanying actor, we simply call the NPC's startFollowing() method. To subsequently stop the actor following the player character around we call its stopFollowing method.

If the actor wants the player character to follow him/her/them around, we need to a FollowAgendaItem for the actor, then define its connectorList property, which is a list of connectors (which in the simplest case could just be rooms, but might need to include doors, stairways and other TravelConnectors) through which the actor wishes to lead the player character. We can optionally also override the specialDesc method to provide a description of where the actor wants to lead the player character, and an arrivingDesc method to describe the leading actor arriving in a new destination having just led the player character there.

Choosing a Method

The six main NPC travel methods - startFollowing(), FollowAgendaItem, scriptedTravelTo(), travelVia(), moveInto() and actionMoveInto() - all have their uses. Which one you usedepends on the situation, and you'll probably use more than one in any given game.

You should use moveInto() when you want to move an actor "by fiat," without triggering any simulation side effects. This is the one to use when you want complete control over what happens, and are willing to take complete responsibility for things like generating arrival and departure messages. This or actionMoveInto() are probably the best options when, for example, the player character's arrival in a location triggers the npc's arrival in the same location from a different direction, or you want to move the npc off-stage from the player character's location.

Use actionMoveInto() when you want to trigger a minimal subset of the notifications normally associated with travel, but you still want control over the NPC's actions during the travel. In particular, this method doesn't trigger any preconditions of the travel, so it's up to you to make sure that any doors that need to be open are open, for example (or else indicate to the player that the npc has opened and close any doors that would need to have been opened and closed).

Use travelVia() when you know (or can easily figure out) which TravelConnector you want the actor to use, and you want the library make the npc use it. This method is best when you want the NPC's travel to be fully simulated, triggering all of the same preconditions and side effects that the player would trigger with a normal "go east" command or the like. This might be the best way to go, if, for example, there might be some barrier, such as a locked door, that would prevent the npc reaching its intended destination.

Use scriptedTravelTo() when you need your npc to appear to be a quasi-autonomous actor going about their own business regardless of what the player character is doing, so that the player character may or may not encounter the npc travelling along their route (and the npc may or may not react to the player character if their paths happen to cross).

Use startFollowing() when you want your npc to act as a sidekick character accompanying the player character on their travels.

Finally, use a FollowAgendaItem when you an npc to lead (or perhaps compel) the player charactert to accompany them.